package com.ian.config;

import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.oauth2.client.oidc.authentication.OidcIdTokenDecoderFactory;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.web.client.RestTemplate;

import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import static org.springframework.security.config.Customizer.withDefaults;

/**
 * 覆盖默认的oauth2代码配置，以及覆盖了 application.yml 中的配置
 * enable OAuth 2.0 login through httpSecurity.oauth2Login()
 * 注：该类可以不存在，也能正常使用oauth2
 *
 * @author Witt
 * @version 1.0.0
 * @date 2022/5/16
 */
@EnableWebSecurity
public class OAuth2LoginSecurityConfig  extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(authorize -> authorize
                // 配置放行 redirect_uri
                .antMatchers("/login/oauth2/code/*","/login/oauth2/authorization",
                        "/login/oauth2/callback/*", "/login/oauth2", "/authorization_code/**").permitAll()
                .anyRequest().authenticated())
                // .oauth2Login(withDefaults());
                .oauth2Login(oauth2Login -> oauth2Login
                        .clientRegistrationRepository(clientRegistrationRepository())
//                        .authorizedClientRepository(authorizedClientRepository())
//                        .authorizedClientRepository(authorizedClientRepository())
                        // If you override the default login page, You need to provide a @Controller with a @RequestMapping("/login/oauth2") that is capable of rendering the custom login page.
//                        .loginPage("/login/oauth2") // default: /login
//                        .authorizationEndpoint(withDefaults())
                        .authorizationEndpoint(authorization -> authorization
//                                .baseUri(authorizationRequestBaseUri())
                                .baseUri("/login/oauth2/authorization")
//                                .authorizationRequestRepository(authorizationRequestRepository())
//                                .authorizationRequestResolver(authorizationRequestResolver())
                        )
//                        .redirectionEndpoint(withDefaults())
                        .redirectionEndpoint(redirection -> redirection
//                                .baseUri(this.authorizationResponseBaseUri())
                                /*
                                The default Authorization Response baseUri (redirection endpoint) is /login/oauth2/code/*, which is defined in OAuth2LoginAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI.
                                If you would like to customize the Authorization Response baseUri, configure it as shown in the following example:

                                 You also need to ensure the ClientRegistration.redirectUri matches the custom Authorization Response baseUri.
                                 具体请参考 googleClientRegistration 对象的创建
                                 */
                                 // 注意：自定义该uri，导致无法获取 gitee 的 userinfo，报404错误，必须在gitee中配置相同的重定向网址，
                                 //         然后在 giteeClientRegistration() 方法中也配置好相同的重定向网址
                                .baseUri("/login/oauth2/callback/*") // default value: "/login/oauth2/code/*"
                        )
                        .tokenEndpoint(withDefaults())
//                        .tokenEndpoint(token -> token
//                                .accessTokenResponseClient(this.accessTokenResponseClient())
//                        )
//                        .userInfoEndpoint(withDefaults())
                        .userInfoEndpoint(userInfo -> userInfo
//                                .userAuthoritiesMapper(this.userAuthoritiesMapper())
                                .userAuthoritiesMapper(userAuthoritiesMapper())
                                // customize DefaultOAuth2UserService or provide your own implementation of OAuth2UserService
                                .userService(oauth2UserService())
                                // implement and configure a delegation-based strategy using an OpenID Connect 1.0 UserService
                                .oidcUserService(oidcUserService())

                        )

                )
                // configure the OidcClientInitiatedLogoutSuccessHandler
                .logout(logout -> logout.logoutSuccessHandler(oidcLogoutSuccessHandler()))
        ;
    }

    /**
     * Example 6. Granted Authorities Mapper Configuration
     * create userAuthoritiesMapper() method to get a GrantedAuthoritiesMapper instance
     * 方法1：
     * @return
     */
    private GrantedAuthoritiesMapper userAuthoritiesMapper() {
        return authorities -> {
            Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
            authorities.forEach(authority -> {
                if (OidcUserAuthority.class.isInstance(authority)) {
                    OidcUserAuthority oidcUserAuthority = (OidcUserAuthority) authority;
                    OidcIdToken oidcIdToken = oidcUserAuthority.getIdToken();
                    OidcUserInfo oidcUserInfo = oidcUserAuthority.getUserInfo();
                    // Map the claims found in idToken and/or userInfo to one or more GrantedAuthority's and add it to mappedAuthorities

                }else if (OAuth2UserAuthority.class.isInstance(authority)) {
                    OAuth2UserAuthority oAuth2UserAuthority = (OAuth2UserAuthority) authority;
                    Map<String, Object> userAttributes = oAuth2UserAuthority.getAttributes();
                    // Map the attributes found in userAttributes to one or more GrantedAuthority's and add it to mappedAuthorities
                }
            });
            return mappedAuthorities;
        };
    }

    /**
     * Example 7. Granted Authorities Mapper Bean Configuration
     * get a GrantedAuthoritiesMapper instance
     * 方法2：
     * Alternatively, you may register a GrantedAuthoritiesMapper @Bean to have it automatically applied to the configuration, as shown in the following example.
     * 然后，configure(HttpSecurity http)方法中，oauth2Login()设置默认值即可： http.oauth2Login(withDefaults());
     */
    @Bean("userAuthoritiesMapper")
    public GrantedAuthoritiesMapper userAuthoritiesMapper2() {
        return authorities -> {
            Set<GrantedAuthority> mappedAuthorities = new HashSet<GrantedAuthority>();
            // ...
            return mappedAuthorities;
        };
    }


    /**
     * Example 8. OAuth2UserService Configuration
     * implement and configure a delegation-based strategy using an OpenID Connect 1.0 UserService
     * @return
     */
    private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
        OidcUserService delegate = new OidcUserService();

        return userRequest -> {
            // Delegate to the default implementation for loading a user
            //   OidcUserService 使用了装饰者模式
            OidcUser oidcUser = delegate.loadUser(userRequest);
            OAuth2AccessToken accessToken = userRequest.getAccessToken();
            Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
            // TODO
            // 1) Fetch the authority information from the protected resource using accessToken
            // 2) Map the authority information to one or more GrantedAuthority's and add it to mappedAuthorities

            // 3) Create a copy of oidcUser but use the mappedAuthorities instead
            oidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());

            return oidcUser;
        };
    }

    /**
     * customize DefaultOAuth2UserService or provide your own implementation of OAuth2UserService
     * @return
     */
    private OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {
        // handle some logic
        return new DefaultOAuth2UserService();
    }


    /**
     * configure the OidcIdTokenDecoderFactory @Bean to default to MacAlgorithm.HS256 for all ClientRegistration
     * @return
     */
    @Bean // 使用 private 修饰方法，编译时报错： @Bean-method in @Configuration must be overridable
    public JwtDecoderFactory<ClientRegistration> idTokenDecoderFactory() {
        OidcIdTokenDecoderFactory idTokenDecoderFactory = new OidcIdTokenDecoderFactory();
        idTokenDecoderFactory.setJwsAlgorithmResolver(clientRegistration -> MacAlgorithm.HS256);
        return idTokenDecoderFactory;
    }

    /**
     * configure the OidcClientInitiatedLogoutSuccessHandler
     * @return
     */
    private LogoutSuccessHandler oidcLogoutSuccessHandler() {
        OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler
                = new OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository());

        // Sets the location that the End-User's User Agent will be redirected to after the logout has been performed at the Provider
        /*
        OidcClientInitiatedLogoutSuccessHandler supports the {baseUrl} placeholder. If used, the application’s base URL, like https://app.example.org, will replace it at request time.
         */
        oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}");
        return oidcLogoutSuccessHandler;
    }


    //////////////////////////////////////////////////////////////////////////////////////////////////

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

//    @Bean
    public ClientRegistrationRepository clientRegistrationRepository() {
        // 该构造器中可以放置多个 ClientRegistration
        return new InMemoryClientRegistrationRepository(
                githubClientRegistration(),
                giteeClientRegistration(),
                googleClientRegistration()
        );
    }

    private ClientRegistration githubClientRegistration() {
        return ClientRegistration.withRegistrationId("github")
                .clientId("810cb8e57c48403ba804")
                .clientSecret("b4c405151bf0dea5c8be46548fec730dcb339172")
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) // default value
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) // default value
                // .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}")
                .redirectUri("{baseUrl}/{action}/oauth2/code/{registrationId}") // default value
                // .scope("openid", "profile", "email", "address", "phone")
                .scope("read:user") // default value
                .authorizationUri("https://github.com/login/oauth/authorize") // default value
                .tokenUri("https://github.com/login/oauth/access_token") // default value
                .userInfoUri("https://api.github.com/user") // default value
                // .userNameAttributeName(IdTokenClaimNames.SUB)
                .userNameAttributeName("id") // default value
                // .jwkSetUri("")
                .clientName("Github") // default value
                .build();
    }

    private ClientRegistration giteeClientRegistration() {
        return ClientRegistration.withRegistrationId("gitee")
                .clientId("de91bdadc6b6b3599bebfa8755850fca0844aae44f13cc5e076110e961d4b59e")
                .clientSecret("6baa392358c93949b4f5e9e443dfa646746eb40c67ffa3eb87decb408d500b77")
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) // default value
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) // default value
                // .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}")
                /*
                默认回调地址
                注意：1 该地址必须和gitee中配置的应用回调地址相同，否则gitee授权页面报错：存在错误，无效的登录回调地址；
                     其他的 自定义回调地址 的注意事项，这里统统不需要。
                 */
                .redirectUri("{baseUrl}/{action}/oauth2/code/{registrationId}") // default value
                /*
                 自定义回调地址
                 注意：1 需要自定义一个Controller方法来处理该回调请求；
                      2 该地址必须和gitee中配置的应用回调地址相同，否则gitee授权页面报错：存在错误，无效的登录回调地址；
                      3 需要 在 SecurityConfig 配置类中配置放行该uri（如此处配置的 /authorization_code），
                            否则一到重定向url，由于没有权限，立即跳到login页面，然后又跳到gitee的授权页面，最终网页提示：你重定向次数过多。
                      4 在请求access_token时（发起post请求 https://gitee.com/oauth/token），必须携带该参数“redirect_uri”，
                            否则报错：org.springframework.web.client.HttpClientErrorException$Unauthorized: 401 Unauthorized: [no body]

                 最终发现，自定义回调地址，还是有问题，本地还是无法认证成功，只是能从gitee获取一些用户信息而已。
                    还是默认的回调地址吧。
                 */
//                .redirectUri("http://localhost:8060/authorization_code")
                // .scope("openid", "profile", "email", "address", "phone")
//                .scope("user") // default value，但是报错：存在错误，请求范围无效、未知或格式不正确
                .authorizationUri("https://gitee.com/oauth/authorize") // default value
                .tokenUri("https://gitee.com/oauth/token") // default value
                .userInfoUri("https://gitee.com/api/v5/user") // default value
                // .userNameAttributeName(IdTokenClaimNames.SUB)
                .userNameAttributeName("id") // default value
                // .jwkSetUri("")
                .clientName("Gitee") // default value
                .build();
    }

    private ClientRegistration googleClientRegistration() {
        return CommonOAuth2Provider.GOOGLE.getBuilder("google")
                .clientId("google-client-id")
                .clientSecret("google-client-secret")
                // ensure the ClientRegistration.redirectUri matches the custom Authorization Response baseUri
                .redirectUri("{baseUrl}/login/oauth2/callback/{registrationId}")
                /*
                OpenID Connect Session Management 1.0 allows the ability to log out the End-User at the Provider using the Client.
                    One of the strategies available is RP-Initiated Logout.
                If the OpenID Provider supports both Session Management and Discovery, the client may obtain the end_session_endpoint URL
                    from the OpenID Provider’s Discovery Metadata. This can be achieved by configuring the ClientRegistration with the issuer-uri,
                    as in the following example:
                 */
                .issuerUri("{baseUrl}")
                .build();
    }
}
